﻿using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Collections.Generic;

namespace Notoric.ViewModel.Elements.CWNotation
{
    /// <summary>
    /// Adapts a <see cref="Model.Elements.CWNotation.Beam"/> instance to a Windows Presentation Foundation view.
    /// </summary>
    public class Beam : Attachment
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Beam"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.Beam"/>
        /// </para>
        /// <para>
        /// The adapted model element instance.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="adaptedElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="NullReferenceException">
        /// <paramref name="owner"/> is <see langword="null"/>.
        /// The values of the <see cref="ViewModelElement.UndoStack"/> and <see cref="ViewModelElement.RedoStack"/> properties
        /// are passed directly to the base class (<see cref="ViewModelElement"/>) constructor. Thus, there is not
        /// an occasion to react with a more appropriate exception.
        /// </exception>
        public Beam(Staff owner, Model.Elements.CWNotation.Beam adaptedElement)
            : base(owner, adaptedElement)
        {
            segmentsCollection = new ObservableCollection<BeamSegment>();
            segments = new ReadOnlyObservableCollection<BeamSegment>(segmentsCollection);
            segmentLevelsIndexes = new int[5];
            for (int i = 0; i < 5; ++i)
                segmentLevelsIndexes[i] = -1;
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override string ElementNameKey
        {
            get { return elementNameKey; }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the <see cref="Model.Elements.CWNotation.Beam.BeamedSymbols"/> collection of the <see cref="ViewModelElement.AdaptedElement"/>
        /// has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler synchronizes the state of the <see cref="beamedSymbolsCollection"/> with the modified one and if the first or last
        /// symbols in the collection has changed, it raises the <see cref="ViewModelElement.PropertyChanged"/> event for the 
        /// <see cref="X1"/> or <see cref="X2"/> properties.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void beamedSymbols_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            var firstSymbol = (beamedSymbolsCollection.Count > 0 ? beamedSymbolsCollection[0] : null);
            var lastSymbol = (beamedSymbolsCollection.Count > 0 ? beamedSymbolsCollection[beamedSymbolsCollection.Count - 1] : null);

            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                    {
                        int index = e.NewStartingIndex;
                        foreach (Model.Elements.CWNotation.DurationalSymbol symbol in e.NewItems)
                        {
                            var staff = symbol.FindOwner<Model.Elements.CWNotation.Staff>();
                            var vmStaff = FindOwner<System>().Staves.First(s => s.AdaptedElement == staff);
                            var vmSymbol = vmStaff.Symbols[staff.IndexOf(symbol)] as DurationalSymbol;
                            vmSymbol.PropertyChanged += new PropertyChangedEventHandler(beamedSymbol_PropertyChanged);
                            beamedSymbolsCollection.Insert(index++, vmSymbol);
                        }
                        break;
                    }
                case Model.Collections.NotifyCollectionChangedAction.Move:
                    {
                        int oldIndex = e.OldStartingIndex;
                        int newIndex = e.NewStartingIndex;
                        foreach (Model.Elements.CWNotation.DurationalSymbol symbol in e.OldItems)
                            beamedSymbolsCollection.Move(oldIndex++, newIndex++);
                        break;
                    }
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    {
                        foreach (Model.Elements.CWNotation.DurationalSymbol symbol in e.OldItems)
                        {
                            var toRemove = beamedSymbolsCollection.First(s => s.AdaptedElement == symbol);
                            toRemove.PropertyChanged -= beamedSymbol_PropertyChanged;
                            beamedSymbolsCollection.Remove(toRemove);
                        }
                        break;
                    }
                case Model.Collections.NotifyCollectionChangedAction.Replace:
                    {
                        foreach (Model.Elements.CWNotation.DurationalSymbol symbol in e.OldItems)
                        {
                            var toRemove = beamedSymbolsCollection.First(s => s.AdaptedElement == symbol);
                            toRemove.PropertyChanged -= beamedSymbol_PropertyChanged;
                            beamedSymbolsCollection.Remove(toRemove);
                        }
                        int index = e.NewStartingIndex;
                        foreach (Model.Elements.CWNotation.DurationalSymbol symbol in e.NewItems)
                        {
                            var staff = symbol.FindOwner<Model.Elements.CWNotation.Staff>();
                            var vmStaff = FindOwner<System>().Staves.First(s => s.AdaptedElement == staff);
                            var vmSymbol = vmStaff.Symbols[staff.IndexOf(symbol)] as DurationalSymbol;
                            vmSymbol.PropertyChanged += new PropertyChangedEventHandler(beamedSymbol_PropertyChanged);
                            beamedSymbolsCollection.Insert(index++, vmSymbol);
                        }
                        break;
                    }
                case Model.Collections.NotifyCollectionChangedAction.Reset:
                    foreach (var symbol in beamedSymbolsCollection)
                        symbol.PropertyChanged -= beamedSymbol_PropertyChanged;
                    beamedSymbolsCollection.Clear();
                    break;
            }

            var newFirstSymbol = (beamedSymbolsCollection.Count > 0 ? beamedSymbolsCollection[0] : null);
            var newLastSymbol = (beamedSymbolsCollection.Count > 0 ? beamedSymbolsCollection[beamedSymbolsCollection.Count - 1] : null);

            if (newFirstSymbol != firstSymbol)
                RaisePropertyChanged("X1");

            if (newLastSymbol != lastSymbol)
                RaisePropertyChanged("X2");

            SetSegments();
        }

        /// <summary>
        /// Occurs when a property of some beamed symbol has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// If the modified property is <see cref="CoreSymbol.Position"/> and the <paramref name="sender"/>
        /// is either the first or the last beamed symbol, the handler raises the <see cref="ViewModelElement.PropertyChanged"/>
        /// event for the <see cref="X1"/> or <see cref="X2"/> property.<br/>
        /// If the modified property is <see cref="DurationalSymbol.BasicDuration"/> or <see cref="Chord.StemDirection"/>
        /// the handler calls the <see cref="SetSegments"/> method.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void beamedSymbol_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Position")
            {
                if (sender == beamedSymbolsCollection[0])
                    RaisePropertyChanged("X1");
                else if (sender == beamedSymbolsCollection[beamedSymbolsCollection.Count - 1])
                    RaisePropertyChanged("X2");
            }
            else if (e.PropertyName == "BasicDuration" || e.PropertyName == "StemDirection")
                SetSegments();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Processes segments at a given duration level.
        /// </summary>
        /// <param name="duration">Duration level.</param>
        private void ProcessSegmentsLevel(ushort duration)
        {
            var level = 5 - Model.Utils.Duration.HighestBit(duration);

            if (duration == 16)
            {
                var segment = segmentsCollection.FirstOrDefault(s => s.Duration == duration);
                if (segment == null)
                    segmentsCollection.Add(new BeamSegment(this, duration, beamedSymbolsCollection[0], beamedSymbolsCollection[beamedSymbolsCollection.Count - 1]));
                else
                {
                    segment.LeftSymbol = beamedSymbolsCollection[0];
                    segment.RightSymbol = beamedSymbolsCollection[beamedSymbolsCollection.Count - 1];
                }
                return;
            }

            DurationalSymbol actualSegmentLeft = beamedSymbolsCollection[0], actualSegmentRight = beamedSymbolsCollection[1];
            StemOrientation actualStemDirection = ((Chord)beamedSymbolsCollection[0]).StemDirection;
            if (beamedSymbolsCollection[1] is Chord)
                actualStemDirection = ((Chord)beamedSymbolsCollection[1]).StemDirection;

            var toRemove = new List<BeamSegment>(from segment in segmentsCollection where segment.Duration == duration select segment);
            foreach (var segment in toRemove)
                segmentsCollection.Remove(segment);

            for (int i = 2; i < beamedSymbolsCollection.Count; ++i)
            {
                bool inverseStemDirection = beamedSymbolsCollection[i] is Chord && ((Chord)beamedSymbolsCollection[i]).StemDirection != actualStemDirection;
                if (inverseStemDirection)
                    actualStemDirection = ((Chord)beamedSymbolsCollection[i]).StemDirection;
                bool belongsToSegment = beamedSymbolsCollection[i].BasicDuration <= duration && !inverseStemDirection;

                if (belongsToSegment)
                {
                    if (actualSegmentRight.BasicDuration <= duration && actualSegmentLeft.BasicDuration > duration)
                        actualSegmentLeft = actualSegmentRight;
                    actualSegmentRight = beamedSymbolsCollection[i];
                }
                else
                {
                    if (!inverseStemDirection)
                    {
                        if (actualSegmentRight.BasicDuration > duration)
                            actualSegmentRight = beamedSymbolsCollection[i];
                        else
                        {
                            segmentsCollection.Add(new BeamSegment(this, duration, actualSegmentLeft, actualSegmentRight));
                            if (i < beamedSymbolsCollection.Count - 1)
                            {
                                actualSegmentLeft = beamedSymbolsCollection[i];
                                actualSegmentRight = beamedSymbolsCollection[++i];
                            }
                            else
                            {
                                actualSegmentLeft = null;
                                actualSegmentRight = null;
                            }
                        }
                    }
                    else
                    {
                        if (actualSegmentRight.BasicDuration <= duration)
                            segmentsCollection.Add(new BeamSegment(this, duration, actualSegmentLeft, actualSegmentRight));
                        if (i < beamedSymbolsCollection.Count - 1)
                        {
                            actualSegmentLeft = beamedSymbolsCollection[i];
                            actualSegmentRight = beamedSymbolsCollection[++i];
                        }
                        else
                        {
                            actualSegmentLeft = actualSegmentRight;
                            actualSegmentRight = beamedSymbolsCollection[i];
                        }
                    }
                }
            }

            if (actualSegmentLeft != null && actualSegmentRight != null)
                segmentsCollection.Add(new BeamSegment(this, duration, actualSegmentLeft, actualSegmentRight));
        }

        /// <summary>
        /// Creates the beam segments.
        /// </summary>
        private void SetSegments()
        {
            if (beamedSymbolsCollection.Count < 2)
            {
                segmentsCollection.Clear();
                for (int i = 0; i < 5; ++i)
                    segmentLevelsIndexes[i] = -1;
                return;
            }

            var shortestDuration = beamedSymbolsCollection.Min(s => s.BasicDuration);
            var maxLevel = 5 - Model.Utils.Duration.HighestBit(shortestDuration);

            for (int i = 4; i > maxLevel; --i)
            {
                if (segmentLevelsIndexes[i] != -1)
                {
                    for (int j = segmentsCollection.Count - 1; j >= segmentLevelsIndexes[i]; --j)
                        segmentsCollection.RemoveAt(j);
                    segmentLevelsIndexes[i] = -1;
                }
            }

            for (ushort duration = 16; duration >= shortestDuration; duration /= 2)
                ProcessSegmentsLevel(duration);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of symbols grouped by this beam.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyObservableCollection{T}"/> of <see cref="DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The collection of symbols grouped by this beam.
        /// </para>
        /// </value>
        public ReadOnlyObservableCollection<DurationalSymbol> BeamedSymbols
        {
            get 
            {
                if (beamedSymbols == null)
                {
                    var adaptedElement = (Model.Elements.CWNotation.Beam)AdaptedElement;

                    beamedSymbolsCollection = new ObservableCollection<DurationalSymbol>();
                    beamedSymbols = new ReadOnlyObservableCollection<DurationalSymbol>(beamedSymbolsCollection);

                    foreach (Model.Elements.CWNotation.DurationalSymbol symbol in adaptedElement.BeamedSymbols)
                    {
                        var staff = symbol.FindOwner<Model.Elements.CWNotation.Staff>();
                        var vmStaff = FindOwner<System>().Staves.First(s => s.AdaptedElement == staff);
                        var vmSymbol = vmStaff.Symbols[staff.IndexOf(symbol)] as DurationalSymbol;
                        vmSymbol.PropertyChanged += new PropertyChangedEventHandler(beamedSymbol_PropertyChanged);
                        beamedSymbolsCollection.Add(vmSymbol);
                    }

                    adaptedElement.BeamedSymbols.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(beamedSymbols_CollectionChanged);
                    SetSegments();
                }
                return beamedSymbols; 
            }
        }

        /// <summary>
        /// Gets the collection of segments of the beam.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyObservableCollection{T}"/> of <see cref="BeamSegment"/>
        /// </para>
        /// <para>
        /// The collection of the segments of this beam.
        /// </para>
        /// </value>
        public ReadOnlyObservableCollection<BeamSegment> Segments
        {
            get 
            {
                if (BeamedSymbols.Count > 0)
                    return segments;
                return segments;
            }
        }

        /// <summary>
        /// Gets the thickness of the beam line.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The thickness (in device independent pixels) of the beam line.
        /// </para>
        /// </value>
        public double Stroke
        {
            get
            {
                return FindOwner<Staff>().Height / 10;
            }
        }

        /// <summary>
        /// Gets the horizontal position of the beam's starting point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in device independent pixels) of the beam's starting point from the left section edge.
        /// </para>
        /// </value>
        public double X1
        {
            get 
            {
                if (BeamedSymbols.Count == 0)
                    return 0;

                return beamedSymbolsCollection[0].Position; 
            }
        }

        /// <summary>
        /// Gets the horizontal position of the beam's ending point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in device independent pixels) of the beam's ending point from the left section edge.
        /// </para>
        /// </value>
        public double X2
        {
            get 
            {
                if (BeamedSymbols.Count == 0)
                    return 0;
                return beamedSymbolsCollection[beamedSymbolsCollection.Count - 1].Position; 
            }
        }

        /// <summary>
        /// Gets the vertical position of the beam's starting point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in device independent pixels) of the beam's starting point from the top section edge.
        /// </para>
        /// </value>
        public double Y1
        {
            get { return FindOwner<Staff>().Bottom + (double)LengthConverter.Convert(GetPropertyValue<double>("Y1"), typeof(double), null, CultureInfo.CurrentCulture); }
        }

        /// <summary>
        /// Gets the vertical position of the beam's ending point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in device independent pixels) of the beam's ending point from the top section edge.
        /// </para>
        /// </value>
        public double Y2
        {
            get { return FindOwner<Staff>().Bottom + (double)LengthConverter.Convert(GetPropertyValue<double>("Y2"), typeof(double), null, CultureInfo.CurrentCulture); }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds a user-friendly name of this element.
        /// </summary>
        private const string elementNameKey = "ViewModel.ElementNames.Beam";
        /// <summary>
        /// Holds the value of the <see cref="BeamedSymbols"/> property.
        /// </summary>
        private ReadOnlyObservableCollection<DurationalSymbol> beamedSymbols;
        /// <summary>
        /// Holds the collection of symbols grouped under this beam.
        /// </summary>
        private ObservableCollection<DurationalSymbol> beamedSymbolsCollection;
        /// <summary>
        /// Holds the value of the <see cref="Segments"/> property.
        /// </summary>
        private ReadOnlyObservableCollection<BeamSegment> segments;
        /// <summary>
        /// Holds the collection of beam segments.
        /// </summary>
        private ObservableCollection<BeamSegment> segmentsCollection;
        /// <summary>
        /// Holds the indexes of distinct segment levels in the <see cref="segmentsCollection"/>.
        /// </summary>
        private int[] segmentLevelsIndexes;
        
        #endregion
    }
}
